package dht.communication;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.zip.GZIPOutputStream;

import dht.message.ContexteMessage;
import dht.message.Message;
import dht.node.Node;
import dht.routage.RoutingStrategy;
import dht.routage.SmartRouter;

/**
 * CommunicationDHT implémente les méthodes d'envoie de messages.
 * @author bouceka
 *
 */

public class CommunicationDHT {

	/**
	 * Fonction de création de paquet, le paquet a pour destinataire soit le voisin 
	 * le plus proche du point specifie dans msg soit un voisin aleatoire.
	 * @param msg un message à envoyer sur le réseau
	 * @return le paquet forgé à partir du message
	 */
	public static DatagramPacket packetForgeToClosestOrRandomNode(Message msg, int port){

		RoutingStrategy rs = new SmartRouter();
		int dim = rs.route(msg.getDestination());
		InetAddress addrr = null;
		try {
			synchronized(Node.myContext){
				
				// On choisit le voisin parmis la liste des voisins dans la dimension dim
				int alea =  0;
				
				alea = (int)(Math.random() * Node.myContext.getVoisinage().get(dim).size());
				
				addrr = InetAddress.getByName(Node.myContext.getVoisinage().get(dim).get(alea).getElem2());
			}
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}

		return CommunicationDHT.packetForgeToKnownNode(msg, addrr, port);
	}

	/**
	 * Fonction qui cree un DatagramPacket pret a l'envoie, utilisé
	 * pour l'envoie du message JOIN vers un noeud connu.
	 * @param msg Le message 
	 * @param addrr L'adresse du noeud connu
	 * @param port Le port de communication 
	 * @return Le DatagramPacket cree
	 */
	public static DatagramPacket packetForgeToKnownNode(Message msg, InetAddress addrr, int port){

		ByteArrayOutputStream monStream = null;
		ObjectOutputStream oos = null;
		byte[] buff;

		monStream = new ByteArrayOutputStream();

		try {
			oos = new ObjectOutputStream(monStream);
			oos.writeObject(msg);
			oos.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}

		buff = monStream.toByteArray();

		return new DatagramPacket(buff,buff.length,addrr,port);
	}

	public static boolean sendMessage(DatagramPacket msg, DatagramSocket sc) {

		try {
			sc.send(msg);
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} 

		return true;
	}

	public static Message receiveMsg(DatagramSocket maSocket) throws IOException, ClassNotFoundException {
		byte buf[] = new byte[65536];
		DatagramPacket packet =new DatagramPacket(buf,buf.length);
		maSocket.receive(packet);
		return (Message)deserialize(packet);
	}

	public static Object deserialize(DatagramPacket packet) throws IOException, ClassNotFoundException {
		ObjectInputStream is =new ObjectInputStream(new ByteArrayInputStream(packet.getData(),packet.getOffset(),packet.getLength()));
		return is.readObject();
	}

	public static String getLocalIp(){

		try{
			Enumeration<NetworkInterface> interfaceN = NetworkInterface.getNetworkInterfaces();		
			while (interfaceN.hasMoreElements() ) {
				NetworkInterface ni=interfaceN.nextElement();
				if (ni.isLoopback()) continue;
				if (ni.isVirtual()) continue;
				if (!ni.isUp()) continue;
				if (ni.getDisplayName().equals("virbr0")) continue;
				Enumeration<InetAddress> ienum = ni.getInetAddresses();
				while (ienum.hasMoreElements()) {
					InetAddress ia = ienum.nextElement();
					String adress = ia.getHostAddress().toString();

					if( adress.length() < 16){ 
						if(!adress.startsWith("127") && (adress.indexOf(":") < 0)){ 
							return adress;
						} 
					}
				}

			}

		}
		catch(Exception e){
			System.out.println("pas de carte reseau");
			e.printStackTrace();
		}

		return null;
	}

	/**
	 * Envoie le contexte avec une connexion TCP, accede au contexte en exclusion mutelle
	 * @param ctx, si ctx est null on envoie le contexte du noeud, sinon on envoie ctx
	 * @param server le noeud distant
	 * @return renvoie -1 si erreur, 0 sinon.
	 */
	public static int sendContexte(ContexteMessage ctx, InetAddress server, int port){
		ObjectOutputStream oos = null;

		Socket scCtx = new Socket();

		try {
			scCtx.connect(new InetSocketAddress(server, port));
		} catch (IOException e) {
			e.printStackTrace();
		}

		try {
			oos = new ObjectOutputStream(new GZIPOutputStream(scCtx.getOutputStream()));
		} catch (IOException e) {
			e.printStackTrace();
		}

		if(ctx == null){
			synchronized(Node.myContext){
				try {
					oos.writeObject(Node.myContext);
					oos.close();
				} catch (IOException e) {
					e.printStackTrace();
					return -1;
				}
			}
		}
		else{
			try {
				oos.writeObject(ctx);
				oos.close();
			} catch (IOException e) {
				e.printStackTrace();
				return -1;
			}
		}

		return 0;
	}

}
